package org.rambadger.command.axon;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.MongoClient;
import com.mongodb.gridfs.GridFS;
import com.yammer.dropwizard.ConfiguredBundle;
import com.yammer.dropwizard.config.Bootstrap;
import com.yammer.dropwizard.config.Environment;
import org.mongojack.JacksonDBCollection;
import org.mongojack.MongoCollection;
import org.mongojack.internal.MongoJackModule;
import org.rambadger.command.CommandConfiguration;

import static org.rambadger.command.providers.SingletonInjectorFactory.getProvider;

/**
 * DropWizard bundle that sets up the MongoDB connection
 */
public class MongoBundle implements ConfiguredBundle<CommandConfiguration> {
    private MongoClient mongo;
    private DB database;
    private GridFS gridFS;

    @Override
    public void initialize(Bootstrap<?> bootstrap) {
        //Unused
    }

    @Override
    public void run(CommandConfiguration configuration, Environment environment) throws Exception {
        MongoConfiguration conf = configuration.getDatabaseConfiguration();
        mongo = new MongoClient(conf.getHost(), conf.getPort());
        environment.manage(new MongoManager(mongo));

        this.database = mongo.getDB(conf.getDatabaseName());
        this.gridFS = new GridFS(database);

        environment.addHealthCheck(new MongoHealthCheck(this.mongo));

        environment.addProvider(getProvider(DB.class, database));
        environment.addProvider(getProvider(GridFS.class, gridFS));
        environment.addProvider(new CollectionProvider(this));
    }


    /**
     * Gets the MongoClient instance.
     *
     * @return the MongoClient instance
     */
    public MongoClient getMongoClient() {
        return mongo;
    }

    /**
     * Gets the database wrapper.
     *
     * @return the database wrapper
     */
    public DB getDatabase() {
        return database;
    }

    /**
     * Gets the GridFS instance.
     *
     * @return the GridFS instance
     */
    public GridFS getGridFS() {
        return gridFS;
    }

    /**
     * Returns a {@link JacksonDBCollection} wrapper for the given class and has a String key type.
     *
     * @param collectionClass the class to make a wrapper for
     * @return the wrapped collection
     *
     * @see #getWrappedCollection(Class, Class)
     */
    public <T> JacksonDBCollection<T, String> getWrappedCollection(Class<T> collectionClass) {
        return getWrappedCollection(collectionClass, String.class);
    }

    /**
     * Returns a {@link JacksonDBCollection} wrapper for the given class and key type.
     * <p>
     *     The collection being wrapped if found by looking for a {@link MongoCollection} annotation on the given collection class, which is mandatory.
     * </p>
     * <p>
     *     If {@link MongoIndexes} is present, {@link #makeIndexes(com.mongodb.DBCollection, Class) makeIndexes} will process the annotation and add the indexes.
     * </p>
     *
     * @param collectionClass the collection type
     * @param keyClass the key type
     * @return the wrapped collection
     */
    public <T, K> JacksonDBCollection<T, K> getWrappedCollection(Class<T> collectionClass, Class<K> keyClass) {
        MongoCollection collectionName = collectionClass.getAnnotation(MongoCollection.class);
        DBCollection collection = database.getCollection(collectionName.name());
        if (collectionClass.isAnnotationPresent(MongoIndexes.class)) {
            makeIndexes(collection, collectionClass);
        }
        return JacksonDBCollection.wrap(collection, collectionClass, keyClass, getWrapperObjectMapper());
    }

    /**
     * Adds the indexes {@link MongoIndexes defined} on klass to collection
     * @param collection the collection to make indexes on
     * @param klass the class to make indexes for
     */
    void makeIndexes(DBCollection collection, Class<?> klass) {
        MongoIndexes indexDefinitions = klass.getAnnotation(MongoIndexes.class);
        //Loop through each index
        for (MongoIndexes.Index index : indexDefinitions.value()) {
            BasicDBObjectBuilder keys = BasicDBObjectBuilder.start();
            //Add the index keys
            for (MongoIndexes.Key key : index.keys()) {
                keys.add(key.name(), (key.ascending()?1:-1));
            }
            BasicDBObjectBuilder config = BasicDBObjectBuilder.start();
            //Name is optional
            if (!index.name().isEmpty()) config.add("name", index.name());
            config.add("unique", index.unique());

            collection.ensureIndex(keys.get(), config.get());
        }
    }

    /**
     * Creates and configures an {@link ObjectMapper} for serialization to the database.
     *
     * @return the configured object mapper
     */
    public static ObjectMapper getWrapperObjectMapper() {
        //We need to provide our own ObjectMapper, otherwise DateTime objects from Joda will serialize in a funky way.
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JodaModule());
        mapper.registerModule(MongoJackModule.INSTANCE);
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
        return mapper;
    }
}
